using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.DirectoryServices.Protocols;
using InfiniTec.Data;
using InfiniTec.Threading;
using AsyncOperation=InfiniTec.Threading.AsyncOperation;

namespace InfiniTec.DirectoryServices
{
	/// <summary>
	/// This class abstracts an LDAP-object to level similar to the System.DirectoryServices.DirectoryEntry
	/// class.
	/// </summary>
	public class Item
	{
		/// <summary>
		/// This event is called when an async refresh operation completes.
		/// </summary>
		public event AsyncCompletedEventHandler RefreshCompleted;

		/// <summary>
		/// This event is called when an async Save operation completes.
		/// </summary>
		public event AsyncCompletedEventHandler SaveCompleted;

		private readonly bool _IsNew;
		private readonly AsyncOperationTracker _OperationTracker = new AsyncOperationTracker();
		private readonly PropertyCollection _Properties;
		private readonly PropertyDefinitionCollection _PropertiesToLoad;

		private Item()
		{
			_PropertiesToLoad = new PropertyDefinitionCollection();
			_Properties = new PropertyCollection();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Item"/> class.
		/// </summary>
		/// <param name="distinguishedName">The distinguished name.</param>
		/// <param name="connection">The connection to use.</param>
		public Item(string distinguishedName, Connection connection)
			: this(distinguishedName, connection, false) {}

		internal Item(string distinguishedName, SearchResultAttributeCollection attributes, Connection connection)
			:
				this(distinguishedName, connection)
		{
			foreach (DirectoryAttribute attribute in attributes.Values)
			{
				Properties.Add(new RawProperty(attribute));
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Item"/> class.
		/// </summary>
		/// <param name="distinguishedName">The distinguished name.</param>
		/// <param name="connection">The connection to use.</param>
		/// <param name="isNew"><see langword="true" />, if the item is being created. <see langword="false" /> otherwise.</param>
		internal Item(string distinguishedName, Connection connection, bool isNew) : this()
		{
			DistinguishedName = distinguishedName;
			Connection = connection;
			_IsNew = isNew;
		}

		/// <summary>
		/// Gets a list containing the properties to load during a refresh operation.
		/// If left empty, all properties are loaded.
		/// </summary>
		/// <value>The properties to load.</value>
		public IList<IPropertyDefinition> PropertiesToLoad
		{
			[DebuggerStepThrough]
			get { return _PropertiesToLoad; }
		}


		/// <summary>
		/// Gets or sets the distinguished name.
		/// </summary>
		/// <value>The distinguished name item.</value>
		public string DistinguishedName { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }

		/// <summary>
		/// Gets or sets the connection to use.
		/// </summary>
		/// <value>The connection to use.</value>
		public Connection Connection { [DebuggerStepThrough]
		get; [DebuggerStepThrough]
		set; }


		/// <summary>
		/// Provides access to the properties of the item.
		/// </summary>
		/// <value>The properties of the item.</value>
		public PropertyCollection Properties
		{
			[DebuggerStepThrough]
			get { return _Properties; }
		}


		/// <summary>
		/// This method is called after an async refresh operation has completed.
		/// </summary>
		/// <param name="e">An <see cref="AsyncCompletedEventArgs"/> instance indicating the success
		/// or failure of the refresh operation.</param>
		protected virtual void OnRefreshCompleted(AsyncCompletedEventArgs e)
		{
			if (RefreshCompleted != null) RefreshCompleted(this, e);
		}

		/// <summary>
		/// Synchronously refreshes the properties of the curren item.
		/// </summary>
		public void Refresh()
		{
			AsyncOperation operation = GetRefreshOperation();
			operation.RunSynchronously();
		}

		/// <summary>
		/// Asynchronously refreshes the properties of this instance.
		/// </summary>
		public void RefreshAsync()
		{
			AsyncOperation operation = GetRefreshOperation();

			operation.Completed += RefreshCompleted;

			operation.Run();
		}

		/// <summary>
		/// Gets the async operation operation used to refresh the item.
		/// </summary>
		/// <returns>An async operation which can be used to refresh the item.</returns>
		public AsyncOperation GetRefreshOperation()
		{
			return new AsyncOperation("Refresh", RefreshInternal(), _OperationTracker, false, null);
		}

		private IEnumerable<OperationAction> RefreshInternal()
		{
			if (Connection == null) throw new InvalidOperationException("The Connection property has not been initialized");

			var request = new SearchRequest(DistinguishedName, (string) null, SearchScope.Base, _PropertiesToLoad.ToStringArray());

			AsyncOperation<DirectoryResponse> operation = Connection.GetSendRequestOperation(request, null);
			yield return new NestedOperation(operation);

			ParseResponse((SearchResponse) operation.Result);
		}

		private void ParseResponse(SearchResponse response)
		{
			SearchResultEntry entry = response.Entries[0];

			_Properties.Clear();

			foreach (DirectoryAttribute attribute in entry.Attributes.Values)
			{
				_Properties.Add(new RawProperty(attribute));
			}
		}

		/// <summary>
		/// This method is called after an async Save operation has completed.
		/// </summary>
		/// <param name="e">An <see cref="AsyncCompletedEventArgs"/> instance indicating the success
		/// or failure of the Save operation.</param>
		protected virtual void OnSaveCompleted(AsyncCompletedEventArgs e)
		{
			if (SaveCompleted != null) SaveCompleted(this, e);
		}

		/// <summary>
		/// Synchronously Saves the properties of the curren item.
		/// </summary>
		/// <param name="lazyCommit">true, if the save operation should return before the changes have been committed to disk. False, otherwise</param>
		public void Save(bool lazyCommit)
		{
			AsyncOperation operation = GetSaveOperation(lazyCommit);
			operation.RunSynchronously();
		}

		/// <summary>
		/// Asynchronously Saves the properties of this instance.
		/// </summary>
		/// <param name="lazyCommit">true, if the save operation should return before the changes have been committed to disk. False, otherwise</param>
		public void SaveAsync(bool lazyCommit)
		{
			AsyncOperation operation = GetSaveOperation(lazyCommit);

			operation.Completed += SaveCompleted;

			operation.Run();
		}


		/// <summary>
		/// Gets the async operation operation used to Save the item.
		/// </summary>
		/// <param name="lazyCommit">true, if the save operation should return before the changes have been committed to disk. False, otherwise</param>
		/// <returns>An async operation which can be used to Save the item.</returns>
		public AsyncOperation GetSaveOperation(bool lazyCommit)
		{
			return new AsyncOperation<RequestProgress>("Save", SaveInternal(lazyCommit), _OperationTracker, false, null);
		}

		private IEnumerable<OperationAction> SaveInternal(bool lazyCommit)
		{
			if (Connection == null) throw new InvalidOperationException("The Connection property has not been initialized");

			var request = new ModifyRequest(DistinguishedName);

			foreach (Property property in _Properties)
			{
				if (!property.HasChanged) continue;

				if (((IPropertyInfo) property).PropertyDefinition.IsMultivalued)
				{
					request.Modifications.Add(PropertyEncoder.GetMultivaluedPropertyModification(property));
				}
				else
				{
					request.Modifications.Add(PropertyEncoder.GetPropertyModification(property));
				}
			}
			foreach (var definition in _Properties.RemovedProperties)
			{
				request.Modifications.Add(new DirectoryAttributeModification
				                          	{Name = definition.Name, Operation = DirectoryAttributeOperation.Delete});
			}
			if (lazyCommit) request.Controls.Add(new LazyCommitControl());

			if (request.Modifications.Count == 0) yield break;

			AsyncOperation<DirectoryResponse> operation = Connection.GetSendRequestOperation(request, null);
			yield return new NestedOperation(operation);
		}
	}
}